﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using MarioArena.Launcher;
using System.Windows.Forms;
using MarioArena.World;
using System.Drawing;
using MarioArena.Characters;
using Microsoft.Xna.Framework;
using MarioArena.HUD;

namespace MarioArena.Multiplayer.TCP
{
    public class TCPConnection
    {
        private const char Delimiter = '\n';

        public LauncherForm Launcher { get; set; }
        public bool Connected { get; set; }

        Stream stream;
        StreamReader streamreader;
        ASCIIEncoding asen = new ASCIIEncoding();
        TcpClient tcpclnt;
        

        #region Singleton
        private static TCPConnection instance = new TCPConnection();
        private TCPConnection()
        {  
        }
        public static TCPConnection Instance
        {
            get
            {
                return instance;
            }
        }
        #endregion


        /// <summary>
        /// Starts the tcp client.
        /// </summary>
        /// <param name="ip">The IP address to connect with.</param>
        /// <param name="port">The port to connect with.</param>
        /// <returns>Returns whether a connection has been established successfully or not.</returns>
        public bool StartClient(string ip, int port)
        {
            bool clientStarted = true;
            try
            {
                tcpclnt = new TcpClient();
                tcpclnt.NoDelay = true;
                tcpclnt.Client.DontFragment = true;
                Console.WriteLine("Connecting...");
                tcpclnt.Connect(ip, port);
                Console.WriteLine("Connected!");
                StartReceiving();
            }

            catch (Exception e)
            {
                Console.WriteLine("Error... " + e.StackTrace);
                clientStarted = false;
                Console.WriteLine("Could not create TCP connection");
            }
            return clientStarted;
        }

        /// <summary>
        /// Starts receiving data from the server.
        /// </summary>
        public void StartReceiving()
        {
            new Thread(new ThreadStart(Receive)).Start();
        }

        /// <summary>
        /// Attempts to receive data from the server.
        /// </summary>
        public void Receive()
        {
            try
            {
                stream = tcpclnt.GetStream();
                streamreader = new StreamReader(stream);

                this.Connected = true;
                while (Connected)
                {
                    string line = streamreader.ReadLine();
                    string[] data = line.Split('_');
                    string messageType = data[0];

                    if (messageType == "0") // Error message
                    {
                        string errorMessage = data[1];
                        Console.WriteLine("TCP ERROR MESSAGE - DISCONNECTING. Message: " + errorMessage);
                        TCPConnection.Instance.Disconnect();
                        MessageBox.Show(errorMessage);
                    }
                    else if (messageType == "1") // Ping request from server. Respond by returning the data.
                    {
                        if (data.Length == 2)
                        {
                            Send("1_" + data[1]);
                        }
                        else Console.WriteLine("Ping data is not correct.");
                    }
                    else if (messageType == "2") // Receive data of all clients except yourself
                    {
                        if (GameManager.Instance.GameState == GameState.Lobby)
                        {
                            // Store the data in the listbox
                            Console.WriteLine("Received (client) data from server: " + line);
                            if (data.Length == 4) // Data of clients received
                            {
                                try
                                {
                                    int id = Convert.ToInt32(data[1]);
                                    Player.Instance.Id = id;
                                    Console.WriteLine("Your id: " + Player.Instance.Id);
                                }
                                catch (Exception)
                                {
                                    MessageBox.Show("Could not retrieve your Id.");
                                }
                                try
                                {
                                    int respawnTime = Convert.ToInt32(data[2]);
                                    Settings.RespawnTime = respawnTime;
                                    Console.WriteLine("Respawn time: " + Settings.RespawnTime + " seconds.");
                                }
                                catch (Exception)
                                {
                                    MessageBox.Show("Could not retrieve Respawn time.");
                                }

                                string opponentsData = data[3];
                                if (!opponentsData.Equals("")) // Check if any opponent data is actually present
                                {
                                    string[] opponents = opponentsData.Split('#');

                                    // Add each opponent to the listbox
                                    for (int i = 0; i < opponents.Length; i++)
                                    {
                                        string opponent = opponents[i];
                                        string[] splitOpponent = opponent.Split(':');
                                        if (splitOpponent.Length == 2)
                                        {
                                            try
                                            {
                                                int opponentId = Convert.ToInt32(splitOpponent[0]);
                                                string opponentName = splitOpponent[1];
                                                AddOpponentToListBox(opponentId, opponentName);
                                            }
                                            catch (Exception) { MessageBox.Show("Could not retrieve opponent ID correctly (on lobby join)."); }
                                        }
                                    }
                                }
                                else Console.WriteLine("No opponents present.");
                                
                            }
                        }
                    }
                    else if (messageType == "3") // New client has entered
                    {
                        if (GameManager.Instance.GameState == GameState.Lobby)
                        {
                            // Add the client to the listbox
                            Console.WriteLine("A new player has joined the lobby: " + line);
                            if (data.Length == 3)
                            {
                                try
                                {
                                    int opponentId = Convert.ToInt32(data[1]);
                                    string opponentName = data[2];
                                    if (!opponentId.Equals(Player.Instance.Id))
                                        AddOpponentToListBox(opponentId, opponentName);
                                }
                                catch (Exception) { MessageBox.Show("Could not retrieve the ID of a joined player correctly."); }
                            }
                        }
                    }
                    else if (messageType == "4") // A client has left the lobby
                    {
                        if (GameManager.Instance.GameState == GameState.Lobby)
                        {
                            if (data.Length == 2)
                            {
                                try
                                {
                                    int opponentId = Convert.ToInt32(data[1]);
                                    RemoveOpponentFromListBox(opponentId);
                                }
                                catch (Exception) { MessageBox.Show("Could not retrieve the ID of a left player correctly."); }
                            }
                        }
                    }
                    else if (messageType == "5") // Ready command, indicating that a client has changed his ready state in the lobby, from the server.
                    {
                        if (GameManager.Instance.GameState == GameState.Lobby)
                        {
                            Console.WriteLine("Ready command received. " + line);
                            if (data.Length == 3)
                            {
                                try
                                {
                                    int id = Convert.ToInt32(data[1]);
                                    string readyState = data[2];
                                    // If the id of the ready command is this player's ID
                                    if (id.Equals(Player.Instance.Id))
                                    {
                                        // Change the button text (besides setting the background color)
                                        if (readyState.Equals("1"))
                                            SetLauncherReadyButtonText("Cancel");
                                        else SetLauncherReadyButtonText("Ready");
                                        Console.WriteLine("Ready: " + readyState);
                                    }
                                    SetListBoxItemReady(id, readyState); // Change background color of the player
                                }
                                catch (Exception) { MessageBox.Show("Could not retrieve a ready command correctly."); };
                            }
                        }
                    }
                    else if (messageType == "6") // Start loading the game
                    {
                        if (GameManager.Instance.GameState == GameState.Lobby)
                        {
                            if (data.Length == 2)
                            {
                                Console.WriteLine("COMMAND RECEIVED: START LOADING GAME");
                                GameManager.Instance.ReceivedLevelString = data[1];
                                LaunchGame();
                            }
                        }
                    }
                    else if (messageType == "7") // All clients are done loading - start the game
                    {
                        Console.WriteLine("COMMAND RECEIVED: All players are done loading. Game should start now. " + line);
                        
                        // Start UDP connection
                        UDPConnection.Instance.StartConnection();

                        // Initialize each opponent for the game
                        foreach (Opponent opponent in GameManager.Instance.Opponents)
                        {
                            // Set the correct character for each opponent
                            opponent.Character = new Mario(false);
                        }

                        Console.WriteLine("Number of opponents: " + GameManager.Instance.Opponents.Count);

                        // Start updating and drawing the game
                        GameManager.Instance.ArePlayersDoneLoading = true;
                        // Start on-screen timer.
                        if (data.Length == 2) // Data format check
                        {
                            int matchDurationInSeconds = Convert.ToInt32(data[1]);
                            Overlay.StartTime(matchDurationInSeconds);
                        }
                        else Console.WriteLine("Received start signal, but without the length of the match.");
                    }
                    else if (messageType == "8") // Receive the pings of all connected clients
                    {
                        if (data.Length == 2) // Data format check
                        {
                            string[] pings = data[1].Split('#');
                            foreach (string ping in pings)
                            {
                                string[] splitPing = ping.Split(':');
                                if (splitPing.Length == 2) // Data format check
                                {
                                    int id = Convert.ToInt32(splitPing[0]);
                                    int currentPing = Convert.ToInt32(splitPing[1]);
                                    if (id.Equals(Player.Instance.Id)) // Update the player's ping
                                    {
                                        Player.Instance.Ping = currentPing;
                                    }
                                    else
                                    {
                                        foreach (Opponent opponent in GameManager.Instance.Opponents) // Update the opponent's ping
                                        {
                                            if (id.Equals(opponent.Id))
                                            {
                                                opponent.Ping = currentPing;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    else if (messageType == "21") // Projectile fired
                    {
                        try
                        {
                            int id = Convert.ToInt32(data[1]);
                            string bulletType = data[2];

                            // The server doesn't use this client's screen size for calculations. Therefore we need to 
                            // subtract half the screen size to basically center the projectile for the camera again.
                            int bulletCameraX = Convert.ToInt32(data[3]) - (GameManager.GraphicsDevice.Viewport.Width / 2);
                            int bulletCameraY = Convert.ToInt32(data[4]) - (GameManager.GraphicsDevice.Viewport.Height / 2);
                            float directionX = float.Parse(data[5]);
                            float directionY = float.Parse(data[6]);
                            Vector2 origin = new Vector2(bulletCameraX, bulletCameraY);
                            Vector2 direction = new Vector2(directionX, directionY);
                            GameManager.Instance.SpawnProjectile(id, bulletType, origin, direction);
                            GameManager.Instance.FireballLaunchSound.Play(0.2f * GameManager.Instance.GameVolume, 0, 0);
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Could not receive [projectile spawn] signal correctly: " + e);
                        };
                    }
                    else if (messageType == "22") // Multiple projectiles fired at once
                    {
                        try
                        {
                            if (data.Length == 8) // 7 attributes
                            {
                                string[] multipleIds = data[1].Split('#');
                                string bulletType = data[2];

                                // The server doesn't use this client's screen size for calculations. Therefore we need to 
                                // subtract half the screen size to basically center the projectile for the camera again.
                                int bulletCameraX = Convert.ToInt32(data[3]) - (GameManager.GraphicsDevice.Viewport.Width / 2);
                                int bulletCameraY = Convert.ToInt32(data[4]) - (GameManager.GraphicsDevice.Viewport.Height / 2);
                                float directionX = float.Parse(data[5]);
                                float directionY = float.Parse(data[6]);
                                int offsetInDegrees = Convert.ToInt32(data[7]);
                                Vector2 origin = new Vector2(bulletCameraX, bulletCameraY);
                                Vector2 direction = new Vector2(directionX, directionY);

                                
                                // Spawn the middle projectile
                                GameManager.Instance.SpawnProjectile(Convert.ToInt32(multipleIds[0]), bulletType, origin, direction);

                                if (multipleIds.Length > 1) // If more than 1 id present
                                {
                                    double midProjectileRotation = Math.Atan2(directionY, directionX);

                                    int dir = -1; // direction respective to the mid projectile; top or bottom
                                    int counter = 1;
                                    for (int i = 1; i < multipleIds.Length; i++)
                                    {
                                        // Create a projectile with an offset
                                        double projectileRotation = midProjectileRotation + MathHelper.ToRadians((counter * 10) * dir);
                                        Vector2 projectileDirection = new Vector2((float)Math.Cos(projectileRotation), (float)Math.Sin(projectileRotation));
                                        projectileDirection.Normalize();
                                        GameManager.Instance.SpawnProjectile(Convert.ToInt32(multipleIds[i]), bulletType, origin, projectileDirection);

                                        if (dir == 1)
                                            counter++; // A projectile has been added both to the top and to the bottom
                                        dir *= -1; // Switch direction
                                    }
                                }

                                GameManager.Instance.FireballLaunchSound.Play(0.2f * GameManager.Instance.GameVolume, 0, 0);
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("Could not receive [projectile spawn] signal correctly: " + e);
                        };
                    }
                    else if (messageType == "31") // Projectile collision with player
                    {
                        // Protocol for projectile collision with player: projectileId_collidedPlayerId_damage_xPos_yPos
                        if (data.Length == 6)
                            try
                            {
                                int projectileId = Convert.ToInt32(data[1]);
                                int collidedPlayerId = Convert.ToInt32(data[2]);
                                int damage = Convert.ToInt32(data[3]);
                                int xPos = Convert.ToInt32(data[4]) - (GameManager.GraphicsDevice.Viewport.Width / 2);
                                int yPos = Convert.ToInt32(data[5]) - (GameManager.GraphicsDevice.Viewport.Height / 2);

                                // Damage player and delete projectile
                                GameManager.Instance.DamagePlayer(collidedPlayerId, projectileId, damage);
                                GameManager.Instance.ExplodeProjectile(projectileId, xPos, yPos);
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [projectile collision with player] signal correctly: " + e);
                            };
                    }
                    else if (messageType == "32") // Projectile collision with tile
                    {
                        // Protocol for projectile collision with tiles: 32_projectileId
                        Console.WriteLine("Received collision with tile! " + line);

                        if (data.Length == 4) // Collision with tiles
                        {
                            try
                            {
                                int projectileId = Convert.ToInt32(data[1]);
                                int xPos = Convert.ToInt32(data[2]) - (GameManager.GraphicsDevice.Viewport.Width / 2);
                                int yPos = Convert.ToInt32(data[3]) - (GameManager.GraphicsDevice.Viewport.Height / 2);
                                Console.WriteLine("X: " + xPos + " Y: " + yPos);
                                // Delete projectile
                                GameManager.Instance.ExplodeProjectile(projectileId, xPos, yPos);
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [projectile collision with tile] signal correctly: " + e);
                            };
                        }
                    }
                    else if (messageType == "41") //Player was killed
                    {
                        //Protocol for killed: 41_diedPlayerId_killerId
                        if (data.Length == 3)
                        {
                            try
                            {
                                int playerId = Convert.ToInt32(data[1]);
                                int killerId = Convert.ToInt32(data[2]);
                                GameManager.Instance.KillPlayer(playerId, killerId);
                                
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [die] signal correctly: " + e);
                            }
                        }
                    }
                    else if (messageType == "42") //Player respawned
                    {
                        //Protocol for respawn: 42_playerId_tileIndexX_tileIndexY	
                        if (data.Length == 4)
                        {
                            try
                            {
                                int playerId = Convert.ToInt32(data[1]);
                                int x = Convert.ToInt32(data[2]);
                                int y = Convert.ToInt32(data[3]);
                                GameManager.Instance.Respawn(playerId, x, y);
                                Console.WriteLine("Received respawn on X:" + x + " Y:" + y);
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [respawn] signal correctly: " + e);
                            }
                        }
                    }
                    else if (messageType == "43") //Player died
                    {
                        //Protocol for die: 43_diedPlayerId_whatHappenedToPlayer
                        if (data.Length == 3)
                        {
                            try
                            {
                                int playerId = Convert.ToInt32(data[1]);
                                string message = data[2];
                                GameManager.Instance.DiePlayer(playerId, message);

                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [die] signal correctly: " + e);
                            }
                        }
                    }
                    else if (messageType == "51") //Time is up
                    {
                        if (data.Length == 2)
                        {
                            try
                            {
                                string[] winnersSplit = data[1].Split('#');
                                int[] winnerIds = new int[winnersSplit.Length];
                                //Convert ids to integers
                                for(int i = 0; i < winnersSplit.Length; i++)
                                    winnerIds[i] = Convert.ToInt32(winnersSplit[i]);

                                Console.WriteLine(winnersSplit[0]);
                                GameManager.Instance.GameEnded(winnerIds);
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine("Could not receive [Game ended] signal correctly: " + e);
                            }
                            
                        }
                    }
                    else if (messageType == "52") //Ten seconds remaining
                    {
                        GameManager.Instance.TenSecondsRemaining();
                    }
                    else if (messageType == "53") //Server shutting down
                    {
                        GameManager.Instance.LeaveMatch();
                    }
                }
                Disconnect();
            }
            catch (Exception e)
            {
                Console.WriteLine("Could not read any data from the stream using TcpClient.Receive(). Exception: " + e);
                Disconnect();
            }

        }

        private string ReadLine(StreamReader reader)
        {
            bool finished = false;
            string line = "";

            while (finished == false)
            {
                int asciiNumber = reader.Read();
                char character = Convert.ToChar(asciiNumber);

                if (!character.Equals(Delimiter))
                    line += character;
                else finished = true;
            }

            return line;
        }

        /// <summary>
        /// Sends a string using TCP.
        /// </summary>
        /// <param name="data">The data to send.</param>
        public void Send(string data)
        {
            try
            {
                byte[] ba = asen.GetBytes(data + "\n");

                // Recreate the tcpclient and stream if the user has disconnected earlier
                if (tcpclnt == null)
                    tcpclnt = new TcpClient();
                if (stream == null)
                    stream = tcpclnt.GetStream();

                // Send the data
                stream.Write(ba, 0, ba.Length);
                stream.Flush();
            }
            catch (Exception e)
            {
                Disconnect();
                Console.WriteLine("TCP connection disconnected due to an error while sending: \n" + e);
            }
        }

        /// <summary>
        /// Disconnects the client.
        /// </summary>
        public void Disconnect()
        {
            Console.WriteLine("TCP DISCONNECTING!");
            this.Connected = false;
            if (streamreader != null)
            {
                streamreader.Close();
                streamreader = null;
            }
            if (stream != null)
            {
                stream.Close();
                stream = null;
            }
            if (tcpclnt != null)
            {
                tcpclnt.Close();
                tcpclnt = null;
            }

            if (GameManager.Instance.GameState == GameState.Lobby)
            {
                try
                {
                    this.Launcher.Invoke((MethodInvoker)delegate
                    {
                        this.Launcher.ReturnToConnectMenu();
                    });
                }
                catch(Exception) // Form has been closed
                {
                }
            }
            else // In-game
            {
                UDPConnection.Instance.Disconnect();
                MessageBox.Show("Disconnected from server. Returning to lobby.");
                Application.Restart();
            }
        }

        /// <summary>
        /// Sets the text on the ready button. This method also takes care of any UI-thread issues.
        /// </summary>
        /// <param name="text">The text to be displayed on the ready button in the launcher.</param>
        private void SetLauncherReadyButtonText(string text)
        {
            this.Launcher.Invoke((MethodInvoker)delegate
            {
                this.Launcher.GetReadyButton().Text = text;
            });
        }

        /// <summary>
        /// Adds an opponent to the list of players in the lobby.
        /// </summary>
        /// <param name="id">The id of the new opponent.</param>
        /// <param name="name">The name of the new opponent.</param>
        private void AddOpponentToListBox(int id, string name)
        {
            this.Launcher.Invoke((MethodInvoker)delegate
            {
                Opponent opponent = new Opponent(id, name);
                this.Launcher.GetPlayerListBox().Items.Add(opponent);
                GameManager.Instance.Opponents.Add(opponent);
            });
        }

        /// <summary>
        /// Removes an opponent from the list of players in the lobby.
        /// </summary>
        /// <param name="id">The id of the opponent to remove.</param>
        private void RemoveOpponentFromListBox(int id)
        {
            this.Launcher.Invoke((MethodInvoker)delegate
            {
                ListBox listBox = this.Launcher.GetPlayerListBox();
                for (int i = 0; i < listBox.Items.Count; i++)
                {
                    if (listBox.Items[i] is Opponent)
                    {
                        Opponent currentOpponent = (listBox.Items[i] as Opponent);
                        if (currentOpponent.Id.Equals(id))
                        {
                            listBox.Items.Remove(currentOpponent);
                        }
                    }
                }
                
                // Remove opponent from the general list of Opponents as well
                for (int i = 0; i < GameManager.Instance.Opponents.Count; i++)
                {
                    Opponent currentOpponent = GameManager.Instance.Opponents[i];
                    if (currentOpponent.Id.Equals(id))
                    {
                        GameManager.Instance.Opponents.Remove(currentOpponent);
                    }
                }
            });
        }

        /// <summary>
        /// Updates the color of the player in the listbox, depending on whether this player/opponent is ready or not.
        /// </summary>
        /// <param name="opponentId">The id of the player to update.</param>
        /// <param name="readyState">Whether the player is now ready ("1") or not ready ("0").</param>
        private void SetListBoxItemReady(int opponentId, string readyState)
        {
            this.Launcher.Invoke((MethodInvoker)delegate
            {
                ListBox listBox = this.Launcher.GetPlayerListBox();
                for (int i = 0; i < listBox.Items.Count; i++)
                {
                    // Find the correct opponent in the playerlist
                    if (listBox.Items[i] is Opponent)
                    {
                        Opponent currentOpponent = (listBox.Items[i] as Opponent);
                        if (currentOpponent.Id.Equals(opponentId))
                        {
                            if (readyState.Equals("1")) // Set ready
                                currentOpponent.IsReadyInLobby = true;
                            else // Set NOT ready
                                currentOpponent.IsReadyInLobby = false;
                        }
                    }
                    else if (Player.Instance.Id.Equals(opponentId))
                    {
                        if (readyState == "1")
                            Player.Instance.IsReadyInLobby = true;
                        else Player.Instance.IsReadyInLobby = false;
                    }
                }

                listBox.Invalidate();
            });
        }

        /// <summary>
        /// Launches the game, closing the lobby and starting the XNA game.
        /// </summary>
        private void LaunchGame()
        {
            this.Launcher.Invoke((MethodInvoker)delegate
            {
                this.Launcher.LaunchGame();
            });
            GameManager.Instance.GameState = GameState.Running;
        }

    }
}
